RDS Data API 経由で Aurora PostgreSQL の Row Level Security (RLS) を使う
いわさです。
先日 re:Invent 2024 の SaaS on AWS セッションの中の以下を視聴していたのですが非常に興味深いアプローチが紹介されていました。
Lambda と RDS を組み合わせる際にコネクション管理の課題から RDS Proxy を採用する方は多いと思います。
そして、マルチテナント SaaS においては PostgreSQL の Row Level Security (RLS) 機能を使う方が多いと思います。
実はこの RDS Proxy と RLS は相性が悪くて、RLS では PostgreSQL のセッション変数を使ってテナントコンテキストを制御することが多いために RDS Proxy の「ピン留め」が発生しやすいです。
悩ましい問題ではあったのですが、上記セッションにて RDS Data API を使って解決するアプローチが紹介されていました。
RDS Data API はコネクションプーリングの仕組みがあるために RDS Proxy 採用理由のひとつであるコネクション管理の問題を解決することが出来ます。[1]
さらに現在の RDS Data API は約 1 年前にアップデートがあって Serverless V2 やプロビジョンドインスタンスでも利用できる上に、V1 の時のような 1,000 RPS レート制限がなくなりました。[2]
私は以前は補助的に利用する感じかなと勝手に解釈していたのですが、現在はメインのデータアクセス用に十分実用性があるように思えます。
このセッションを見ていてなるほどと思ったのですが、RLS も使えるのであればマルチテナント SaaS におけるコネクション管理、テナント分離の課題も解決することが出来そうです。
ということで、今回は RDS Data API を使って Aurora PostgreSQL の RLS を使ってみましたのでその様子を紹介します。
Aurora PostgreSQL と RLS の設定
Aurora PostgreSQL インスタンスを作成し、RLS の設定をしていきたいと思います。
まずは Aurora クラスターを新規作成します。
のちほど RDS Data API を使うので有効化だけここでしちゃいます。
次の AWS 公式ブログを参考にマルチテナントなテーブルを作成し、RLS ポリシーを有効化します。
% psql -h hoge0120psql2.cluster-cj6qesyw8nvh.ap-northeast-1.rds.amazonaws.com -U postgres -d hogedb
Password for user postgres:
psql (14.15 (Homebrew), server 15.4)
WARNING: psql major version 14, server major version 15.
Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.
hogedb=> CREATE TABLE tenant_user (user_id INT PRIMARY KEY, tenant_id INT, name VARCHAR(255));
CREATE TABLE
hogedb=> ALTER TABLE tenant_user ENABLE ROW LEVEL SECURITY;
ALTER TABLE
hogedb=> CREATE POLICY tenant_user_isolation_policy ON tenant_user USING (tenant_id = current_setting('app.current_tenant')::INT);
CREATE POLICY
hogedb=> INSERT INTO tenant_user VALUES (1, 100, 'user1'), (2, 100, 'user2'), (3, 200, 'user3'), (4, 200, 'user4');
INSERT 0 4
hogedb=> select * from tenant_user;
user_id | tenant_id | name
---------+-----------+-------
1 | 100 | user1
2 | 100 | user2
3 | 200 | user3
4 | 200 | user4
(4 rows)
テストデータの作成まで行いました。
管理ユーザーであれば全件取得出来ます。
ロールを切り替えてみます。
hogedb=> SET ROLE hoge1;
SET
hogedb=> select * from tenant_user;
ERROR: invalid input syntax for type integer: ""
hogedb=> SET app.current_tenant = 200;
SET
hogedb=> select * from tenant_user;
user_id | tenant_id | name
---------+-----------+-------
3 | 200 | user3
4 | 200 | user4
(2 rows)
セッション変数で設定したテナントの情報だけ取得出来ましたね。
なお、今回 RDS Proxy とピン留めの確認は行いませんが参考情報として紹介します。
要は上記セッション変数の設定で固有の接続と認識されてその接続が再利用されなくなってしまいます。
RDS Data API で RLS を使ってみる
RDS Data API の利用方法は以下です。今回は AWS CLI 経由で呼び出してみます。
クエリの実行にはaws rds-data execute-statement
を行えば良いみたいです。
まずは先ほどと同じ SQL ステートメントをひととおり実行してみます。
% cat hoge.json
{
"resourceArn": "arn:aws:rds:ap-northeast-1:123456789012:cluster:hoge0120psql2",
"secretArn": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:rds!cluster-14977ff5-5973-4f78-8b54-891566f3bd2d-8A1u6P",
"database": "hogedb"
}
% aws rds-data execute-statement --cli-input-json file://hoge.json --profile account1 --sql "SET ROLE hoge1;"
{
"numberOfRecordsUpdated": 0,
"generatedFields": []
}
% aws rds-data execute-statement --cli-input-json file://hoge.json --profile account1 --sql "SET app.current_tenant = 100;"
{
"numberOfRecordsUpdated": 0,
"generatedFields": []
}
% aws rds-data execute-statement --cli-input-json file://hoge.json --profile account1 --sql "select * from tenant_user;"
{
"records": [
[
{
"longValue": 1
},
{
"longValue": 100
},
{
"stringValue": "user1"
}
],
[
{
"longValue": 2
},
{
"longValue": 100
},
{
"stringValue": "user2"
}
],
[
{
"longValue": 3
},
{
"longValue": 200
},
{
"stringValue": "user3"
}
],
[
{
"longValue": 4
},
{
"longValue": 200
},
{
"stringValue": "user4"
}
]
],
"numberOfRecordsUpdated": 0
}
あれ!?全件取得されてしまいました。なんと。
トランザクションを使う
execute_statement
を単独で呼び出す場合、各実行は独立したものとして扱われるのでセッション状態の変更が維持されません。
よって、解決策としてトランザクションを明示的に使う必要があります。
今度はaws rds-data begin-transaction
を使って取得したトランザクション ID をその後も使いまわしてみましょう。
% cat transaction.json
{
"resourceArn": "arn:aws:rds:ap-northeast-1:123456789012:cluster:hoge0120psql2",
"secretArn": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:rds!cluster-14977ff5-5973-4f78-8b54-891566f3bd2d-8A1u6P",
"database": "hogedb"
}
% aws rds-data begin-transaction --cli-input-json file://transaction.json --profile account1
{
"transactionId": "ZjhiYmZjZGEtMTBjNS00ZWFjLTkyODEtOWQxNTc1NzcxNjNjOmRiLVhOWkIyQzVaMkwyTldUQzVCRks0TE1KS1NZ"
}
% aws rds-data execute-statement --cli-input-json file://hoge.json --profile account1 --sql "SET ROLE hoge1;" --transaction-id ZjhiYmZjZGEtMTBjNS00ZWFjLTkyODEtOWQxNTc1NzcxNjNjOmRiLVhOWkIyQzVaMkwyTldUQzVCRks0TE1KS1NZ
{
"numberOfRecordsUpdated": 0,
"generatedFields": []
}
% aws rds-data execute-statement --cli-input-json file://hoge.json --profile account1 --sql "SET app.current_tenant = 100;" --trans
action-id ZjhiYmZjZGEtMTBjNS00ZWFjLTkyODEtOWQxNTc1NzcxNjNjOmRiLVhOWkIyQzVaMkwyTldUQzVCRks0TE1KS1NZ
{
"numberOfRecordsUpdated": 0,
"generatedFields": []
}
% aws rds-data execute-statement --cli-input-json file://hoge.json --profile account1 --sql "select * from tenant_user;" --transact
ion-id ZjhiYmZjZGEtMTBjNS00ZWFjLTkyODEtOWQxNTc1NzcxNjNjOmRiLVhOWkIyQzVaMkwyTldUQzVCRks0TE1KS1NZ
{
"records": [
[
{
"longValue": 1
},
{
"longValue": 100
},
{
"stringValue": "user1"
}
],
[
{
"longValue": 2
},
{
"longValue": 100
},
{
"stringValue": "user2"
}
]
],
"numberOfRecordsUpdated": 0
}
お、今度はセッション変数が効いて対象テナントのデータへのみアクセス出来ましたね。成功です。
冒頭の動画によると厳密にはピン留めが発生しているそうなのですが、危険な状態になった際には RDS Data API がうまく接続を確保してくれるとのこと。
考慮事項:料金
RDS Data API は追加料金が発生します。
リクエストサイズによって単位が若干変わりますが、100 万リクエスト xx ドルのような料金形態になってます。
テナントごとのインフラコスト算出など必要になるケースもあると思うので、テナントごとに呼び出しコストがかかっている点を認識しておきましょう。
さいごに
本日は RDS Data API 経由で Aurora PostgreSQL の Row Level Security (RLS) を使ってみました。
なんでも RDS Data API にすれば良いとは思いませんが、RDS Proxy とか VPC Lambda など使うのであれば、レート制限がなくなった新しい RDS Data API は良い選択肢かもしれません。